package SFT;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;

import WavFile.WavFile;
import WavFile.WavFileException;


 /** SFTAlgorithm.java
  * Central class of the project. Includes almost all of the main algorithmical features.
  * @author Baruch Weizman, Oren Gilon, Barak Arkis*/
public class SFTAlgorithm {
	/*constants that need to be tweaked:*/
	/*if the score of a segment is smaller than this threshold, it is "good enough"*/
	private static final double MIN_SCORE_THRESHOLD = 0.01;
	/*minimal length (in MS) of a candidate for the periodic segment*/
	private static final long PER_SEG_LENGTH = 4;
	/*increment size of the candidates, in MS.*/
	private static final double PER_SEG_INCREMENT_SIZE = 0.2;
	/*number of increments to be checked for a good candidate.
	 * i.e. the candidates are of length:
	 * PER_SEG_LENGTH,PER_SEG_LENGTH+PER_SEG_INCREMENT_SIZE, ..., PER_SEG_LENGTH+PER_SEG_INCREMENT_SIZE*NUM_OU_INCREMENTS*/
	private static final int NUM_OF_INCREMENTS = 81;
	/*initial tau for the algorithm is INF_NORM/START_TAU_FRACTION*/
	private static final double START_TAU_FRACTION = 16;
	
	
	/**The central function. Receives parameters-
	 * WAVFileName: Name of the file to be analyzed.
	 * Threshold: the fraction of the function that can be missed. i.e. the returned function satisfies, approximately:
	 * 		||INPUT_FUNCTION-RETURNED_FUNCTION||<threshold.
	 * segmentSize: the time period, in MS, that every time element returned should encompass.
	 * overlap: the overlap, in MS, between two neighboring time elements.
	 * 
	 * Algorithm Applies SFT algorithm to a representative of each segment. Output of SFT algorithm
	 * is encapsulated in an object of type OutputClass, and returned.*/	
	
	public static OutputClass GetHeavyCharacters(String WAVFileName, double threshold, double segmentSize, double overlap) {
		OutputClass opt=new OutputClass();
		int perSegLength=1, numofTests=10, perSegIncrementSize=1;
		WavFile file;
		long FrameRate=1;
		try {
			/*opens the WAV file and extracts all the data we need from it.*/
			file = WavFile.openWavFile(new File(WAVFileName));
			FrameRate=file.getSampleRate();
			opt.setSampleRate(FrameRate);
			file.close();
			segmentSize=FrameRate*segmentSize/1000;
			overlap=FrameRate*overlap/1000;
			perSegLength=(int) (FrameRate*PER_SEG_LENGTH/1000);
			perSegIncrementSize=(int) (FrameRate*PER_SEG_INCREMENT_SIZE/1000);
		} catch (IOException e) {
			e.printStackTrace();
		} catch (WavFileException e) {
			e.printStackTrace();
		}
		
		/*creates the function from the WAV file*/
		Function func= new WAVFunction(WAVFileName, 0);
		Pair segment=new Pair(-1, (int) (overlap-1));
		
		/*runs until the entire file has been covered by segments*/
		while(segment.cdr()<func.maxVal()){
			segment= new Pair((int) (segment.cdr()-overlap+1), (int) Math.min(segment.cdr()-overlap+segmentSize, func.maxVal()));
			/*find the periodic segment, which is a short segment that "represents" the entire segment*/
			Pair periodicSegment2,  periodicSegment = findPeriod(segment, perSegLength, perSegIncrementSize, func, numofTests);
			double weight1=Double.MAX_VALUE;
			double weight2=Double.MAX_VALUE;
			TimeElements tm=new TimeElements(0,0);
			double tau=func.eucNorm(periodicSegment.car(), periodicSegment.cdr())/START_TAU_FRACTION;
			SFTImplementation.mB=10;
			/*run until a large enough fraction of the function has been covered.*/
			while(SFTImplementation.mB<(periodicSegment.cdr()-periodicSegment.car()+1)){
				/*run SFT on the periodic segment*/
				tm=SFTImplementation.SFT(periodicSegment.car(), periodicSegment.cdr(), tau, 0.1, 1, func);
				/*extract the heaviest coefficients from the returned time element- this is to deal with the issue of frequencies
				 * that do not complete a whole number of periods and thus "break"*/
				weight1=calcWeight(tm, func);
				tm=getHeavyFromTm(periodicSegment, tm, 1.1);
				tm.addConjugate();
				/*subtracts the heavy coefficients from the function*/
				Function dif=new diffFuncTm(tm, func);
				/*finds a better periodic segment for the difference function*/
				periodicSegment2 = findPeriod(segment, perSegLength, perSegIncrementSize, dif, numofTests);
				/*finds the heavy coefficients in the new segment, and merges them to the original time element*/
				long temp=SFTImplementation.mB;
				TimeElements newTm = SFTImplementation.SFT(periodicSegment2.car(), periodicSegment2.cdr(), tau, 0.1, 1, dif);
				SFTImplementation.mB=temp;
				weight2=calcWeight(newTm, dif);
				newTm=getHeavyFromTm(periodicSegment, newTm, 1.1);
				newTm.addConjugate();
				tm.merge(newTm);
				/*halves tau for the next iteration*/
				tau=tau/2;
				if(weight1<threshold || weight2<threshold)
					break;
			}
			tm.setFrom(segment.car());
			tm.setTo(segment.cdr());
			opt.getFrameList().add(tm);
		}
		/*see documentation of fillSeconds() in output class*/
		opt.fillSeconds();
		return opt;
	}
	
	/**receives a time element and returns a time element containing only the heaviest coefficients from the
	 * time element.
	 * The definition of a heavy coefficient is that its amplitude is at least 1/d of the amplitude of
	 * the next biggest coefficient.*/
	private static TimeElements getHeavyFromTm(Pair segment, TimeElements tm, double d) {
		Double[] freqs=tm.sortByFrequency();
		double size;
		TimeElements newTm=new TimeElements(segment.car(), segment.cdr());
		/*find heaviest coefficient*/
		int index=findMaxExceptZero(freqs, tm);
		if(index<0){
			return newTm;
		}
		do{
			size=Complex.Abs(tm.getHeavyCoeffs().get(freqs[index]));
			newTm.getHeavyCoeffs().put(freqs[index], tm.getHeavyCoeffs().get(freqs[index]));
			/*find the conjugate of the added coefficient, and add it as well*/
			/*mark added coefficients as taken*/
			freqs[index]=new Double(0);
			/*prepare for next iteration*/
			index=findMaxExceptZero(freqs, tm);
			/*checks the next coefficient is heavy as well*/
		}while(index>=0 && d*Complex.Abs(tm.getHeavyCoeffs().get(freqs[index])) >= size);
		return newTm;
	}
	/**returns the index of the Double in freqs whose amplitude in the tm is the greatest.
	 * Ignores all members of freqs whose value is 0.0*/
	private static int findMaxExceptZero(Double[] freqs, TimeElements tm) {
		int index=-1;
		double size=Double.MIN_VALUE;
		for (int i=0; i<freqs.length; i++) {
			if(freqs[i].doubleValue()==0)
				continue;
			double temp=Complex.Abs(tm.getHeavyCoeffs().get(freqs[i]));
			if(temp>size){
				size=temp;
				index=i;
			}
		}
		return index;
	}

	/**finds a periodic segment that is a representative for the entire segment. this is done by iterating over many
	 * different segments and giving each one a score, as defined by "calcScore()". the segment with the lowest score
	 * is the one returned.*/
	private static Pair findPeriod(Pair segment, int perSegLength, int perSegIncrementSize, Function func, int numofTests) {
		double min_score= Double.MAX_VALUE;
		Pair curr_cand=null;
		int i=0;
		do{
			List<Pair> candidates=createCandidates(segment, Math.min(segment.length(), perSegLength+i*perSegIncrementSize));
			for (Pair candidate : candidates) {
				double score=calcScore(func, candidate, candidates, numofTests);
				if(score<min_score){
					min_score=score;
					curr_cand=candidate;
				}
			}
			i++;
		}while(min_score>MIN_SCORE_THRESHOLD && perSegLength+i*perSegIncrementSize<=segment.length() && i<NUM_OF_INCREMENTS);
		return curr_cand;
	}

	/**calculates a candidates score by randomly choosing other candidates of the same length, and calculating
	 * 	||FUNC_ON_CANDIDATE-FUNC_ON_RANDOM_SEGMENT||
	 * The score is the average of these numbers.*/
	private static double calcScore(Function func,Pair candidate, List<Pair> candidates, int numofTests) {
		Pair[] PairArr= new Pair[numofTests];
		Random rand=new Random();
		for(int i=0;i<PairArr.length;i++){
			PairArr[i]=candidates.get(rand.nextInt(candidates.size()));
		}
		double score=0;
		for(int i=0;i<PairArr.length;i++){
			Function diff=new diffFunc(candidate,PairArr[i],func);
			score+=diff.eucNorm(0, diff.maxVal());
		}
		return score/numofTests;
	}

	/**returns a list that is the devision of the segment into concurrent segments of length
	 * perSegLength frames.*/
	private static List<Pair> createCandidates(Pair segment, int perSegLength) {
		List<Pair> list=new ArrayList<Pair>();
		int numSegs=(int) Math.floor(segment.length()/(double)perSegLength);
		for(int segNo=0; segNo<numSegs;segNo++){
			int start=segment.car()+perSegLength*segNo;
			int end=start+perSegLength-1;
			list.add(new Pair(start, end));
		}
		return list;
	}

	/**@deprecated
	 *  an attempt to compress a WAV file using our algorithm. Due to poor quality this should no be used! */
	public static void compressWAV(String WAVFileName, String OutputFileName){
		OutputClass out=SFTAlgorithm.GetHeavyCharacters(WAVFileName, 0.9, 10, 0);
		try {
			FileOutputStream fileOut = new FileOutputStream(OutputFileName);
			ObjectOutputStream out1 = new ObjectOutputStream(fileOut);
			out1.writeObject(out);
		} catch (Exception e) {System.out.println("Bad thing happened!");}
	}
	
	/**@deprecated
	 * an attempt to compress a WAV file using our algorithm. Due to poor quality this should no be used! */
	public static void decompressWAV(String compressedFile, String OutputWAV){
		try {
			FileInputStream fileIn = new
			FileInputStream(compressedFile);
			ObjectInputStream in = new
			ObjectInputStream(fileIn);
			OutputClass out = (OutputClass)in.readObject();
			WAVFunction.exportToWAV(OutputWAV, out);
		} catch (Exception e) {System.out.println("Bad thing happened!!");}
	}
	
	/**approximates
	 * 	||func-FUNCTION_DEFINED_BY_TM||*/
	private static double calcWeight(TimeElements tm, Function func) {
			int size=(int) (tm.getTo()-tm.getFrom()+1);
			Complex temp;
			double sum=0;
			Random rand=new Random();
			long num=SFTImplementation.mB;
			/*Iterates over randomly chosen indexes at which the norm is calculated*/
			for(int i=0; i<Math.min(size, num); i++){
				int r=rand.nextInt(size);
				temp=func.apply((int) (tm.getFrom()+r));
				/*calculates the function defined by the time element and subtracts it from func*/
				for (double key : tm.getHeavyCoeffs().keySet()) {
					temp=Complex.Sub(temp, Complex.Mult(tm.getHeavyCoeffs().get(key), Complex.Chi(key, r, 1)));
					
				}
				sum+=Complex.Abs(temp)*Complex.Abs(temp);
			}
			double euc=func.eucNorm((int)tm.getFrom(), (int) tm.getTo());
			/*Checks for a special case in which one of the functions is
			 * the 0 function*/
			if(euc==0){
				if(Math.sqrt((sum/Math.min(size,num)))==0)
					return 0;
				return 1;
			}
			return Math.sqrt((sum/Math.min(size,num)))/euc;
				
	}
}
